# Simple function — no parameters, no return value
def print_welcome():
print("Welcome to the Golf Data Science Course!")
print_welcome()Functions
Functions are reusable blocks of code. In this notebook, we’ll build a library of golf utility functions.
What You’ll Learn
- Defining functions with
def - Parameters, return values, and type hints
- Default arguments and keyword arguments
- Returning multiple values
- Lambda functions
- The
if __name__ == "__main__"pattern
Concept: Why Functions?
Without functions, you’d copy-paste the same scoring logic every time you need it. Functions solve three problems:
- Reusability — Write once, use everywhere
- Abstraction — Hide complexity behind a simple name (
calculate_handicap()is easier to read than 10 lines of math) - Testability — You can verify a function works correctly in isolation
Think of a function like a specific club in your bag — it has a clear purpose, you know what it does, and you reach for it when the situation calls for it.
Code: Defining Functions
Basic Structure
# Parameters and return value
def calculate_relative_score(score: int, par: int) -> int:
"""Calculate score relative to par."""
return score - par
result = calculate_relative_score(78, 72)
print(f"78 on a par 72 = {result:+d}") # +6# Type hints make your code self-documenting
# They don't enforce anything at runtime — they're for readability and tooling
def format_score(score: int, par: int) -> str:
"""Format a score relative to par (e.g., +3, -1, E)."""
diff = score - par
if diff > 0:
return f"+{diff}"
elif diff < 0:
return str(diff)
return "E"
print(format_score(68, 72)) # -4
print(format_score(72, 72)) # E
print(format_score(78, 72)) # +6Default Arguments
Give parameters default values so callers don’t always have to specify them.
def calculate_handicap_differential(
score: int,
course_rating: float,
slope_rating: int = 113, # 113 is the standard (average) slope
) -> float:
"""Calculate a handicap differential for a single round.
Formula: (Score - Course Rating) × 113 / Slope Rating
"""
return (score - course_rating) * 113 / slope_rating
# North Park: course rating 71.1, slope 117
diff = calculate_handicap_differential(82, 71.1, 117)
print(f"Score 82 at North Park: differential = {diff:.1f}")
# Using default slope (113)
diff = calculate_handicap_differential(82, 71.1)
print(f"Score 82 at average course: differential = {diff:.1f}")Keyword Arguments
Use parameter names to make function calls more readable and order-independent.
# These are all equivalent
diff1 = calculate_handicap_differential(82, 71.1, 117)
diff2 = calculate_handicap_differential(score=82, course_rating=71.1, slope_rating=117)
diff3 = calculate_handicap_differential(82, slope_rating=117, course_rating=71.1)
print(f"All the same: {diff1:.1f} = {diff2:.1f} = {diff3:.1f}")Returning Multiple Values
Python functions can return multiple values as a tuple.
def analyze_round(scores: list[int], pars: list[int]) -> tuple[int, int, dict]: """Analyze a round and return total, relative score, and scoring breakdown.""" total = sum(scores) total_par = sum(pars) relative = total - total_par breakdown = { "albatrosses": 0, "eagles": 0, "birdies": 0, "pars": 0, "bogeys": 0, "doubles+": 0, } for score, par in zip(scores, pars): diff = score - par if diff <= -3: breakdown["albatrosses"] += 1 elif diff == -2: breakdown["eagles"] += 1 elif diff == -1: breakdown["birdies"] += 1 elif diff == 0: breakdown["pars"] += 1 elif diff == 1: breakdown["bogeys"] += 1 else: breakdown["doubles+"] += 1 return total, relative, breakdown# Unpack the return valuespars = [4, 3, 5, 4, 4, 3, 5, 4, 4, 4, 3, 5, 4, 4, 3, 5, 4, 4]scores = [4, 2, 5, 5, 4, 3, 6, 4, 4, 5, 3, 4, 5, 4, 3, 5, 4, 5]total, relative, breakdown = analyze_round(scores, pars)print(f"Score: {total} ({format_score(total, sum(pars))})")print(f"Breakdown: {breakdown}")Variable Arguments (*args)
Accept any number of arguments.
def average_score(*scores: int) -> float:
"""Calculate the average of any number of scores."""
if not scores:
return 0.0
return sum(scores) / len(scores)
print(f"Average: {average_score(78, 82, 75, 80):.1f}")
print(f"Single round: {average_score(72):.1f}")Lambda Functions
Small, anonymous functions for simple operations — often used for sorting and filtering.
# Sort players by handicapplayers = [ {"name": "Bear Woods", "handicap": 2.1}, {"name": "Brian Kolowitz", "handicap": 13.9}, {"name": "Sam Shanks", "handicap": 18.7}, {"name": "Bobby Bogey", "handicap": 25.3},]# Lambda: a one-line functionsorted_players = sorted(players, key=lambda p: p["handicap"])for p in sorted_players: print(f"{p['name']:20s} HCP {p['handicap']:.1f}")# Filter: find all sub-80 rounds
round_scores = [78, 82, 75, 80, 77, 83, 79, 76, 81, 68]
sub_80 = list(filter(lambda s: s < 80, round_scores))
print(f"Sub-80 rounds: {sub_80}")
# Map: convert scores to relative-to-par
par = 72
relative = list(map(lambda s: s - par, round_scores))
print(f"Relative to par: {relative}")Building a Reusable Module
Let’s combine our functions into something reusable. In a real project, you’d save this as golf_utils.py.
# ── Golf Utilities ────────────────────────────────────────────────
def format_score(score: int, par: int) -> str:
"""Format a score relative to par (e.g., +3, -1, E)."""
diff = score - par
if diff > 0:
return f"+{diff}"
elif diff < 0:
return str(diff)
return "E"
def scoring_name(score: int, par: int) -> str:
"""Return the golf name for a hole score."""
names = {-3: "Albatross", -2: "Eagle", -1: "Birdie", 0: "Par",
1: "Bogey", 2: "Double Bogey", 3: "Triple Bogey"}
return names.get(score - par, f"{score - par:+d}")
def handicap_differential(
score: int, course_rating: float, slope_rating: int = 113
) -> float:
"""Calculate handicap differential: (Score - CR) × 113 / Slope."""
return (score - course_rating) * 113 / slope_rating
def calculate_handicap(differentials: list[float]) -> float:
"""Calculate handicap index from a list of differentials.
Uses the best 8 of the last 20 differentials.
"""
if len(differentials) < 5:
return max(differentials) # Simplified for few rounds
# Take the best (lowest) 8 of up to 20 most recent
recent = differentials[-20:]
best_8 = sorted(recent)[:8]
return sum(best_8) / len(best_8)
# ── Test it ───────────────────────────────────────────────────────
# Simulate 10 rounds at North Park (CR 71.1, Slope 117)
scores = [82, 78, 85, 80, 79, 83, 77, 81, 84, 76]
diffs = [handicap_differential(s, 71.1, 117) for s in scores]
print("Round differentials:")
for score, diff in zip(scores, diffs):
print(f" Score {score} → diff {diff:.1f}")
hcp = calculate_handicap(diffs)
print(f"\nHandicap Index: {hcp:.1f}")The if __name__ == "__main__" Pattern
When you save functions in a .py file, you often want to include test code that only runs when the file is executed directly — not when it’s imported.
# golf_utils.py
def format_score(score: int, par: int) -> str:
# ... function code ...
def scoring_name(score: int, par: int) -> str:
# ... function code ...
if __name__ == "__main__":
# This only runs when you do: python golf_utils.py
# It does NOT run when you do: from golf_utils import format_score
print(format_score(68, 72)) # -4
print(scoring_name(3, 4)) # BirdieAI: Functions and Code Design
Exercise 1: AI-Generated Handicap Calculator
Try this prompt:
“Write a Python function that calculates a USGA handicap index. It should take a list of (score, course_rating, slope_rating) tuples, use the official formula, and handle edge cases like fewer than 5 rounds. Include type hints and a docstring.”
Evaluate: - Does it use the correct formula? (Check against the USGA rules) - Does it handle the “best N of 20” rule correctly? - How does it compare to our calculate_handicap function above?
Exercise 2: Refactoring with AI
Take this repetitive code and ask AI to refactor it into functions:
scores1 = [4, 3, 5, 5, 4, 3, 6, 4, 4]
pars1 = [4, 3, 5, 4, 4, 3, 5, 4, 4]
total1 = sum(scores1)
birdies1 = sum(1 for s, p in zip(scores1, pars1) if s < p)
print(f"Round 1: {total1}, {birdies1} birdies")
scores2 = [5, 3, 4, 4, 5, 3, 5, 5, 4]
pars2 = [4, 3, 5, 4, 4, 3, 5, 4, 4]
total2 = sum(scores2)
birdies2 = sum(1 for s, p in zip(scores2, pars2) if s < p)
print(f"Round 2: {total2}, {birdies2} birdies")“Refactor this code to eliminate duplication. Create a function and use it for both rounds.”
This teaches you to recognize when AI is good at mechanical refactoring.
Exercise 3: Explain Lambda
If lambda functions feel confusing, ask AI:
“Explain Python lambda functions with 5 examples, starting simple and getting progressively more complex. Show the equivalent
deffunction for each lambda.”
# Paste and test AI-generated code hereSummary
def function_name(param: type) -> return_type:— function definition with type hints- Default arguments let callers skip optional parameters
- Keyword arguments make calls more readable:
handicap_differential(score=82, course_rating=72.4) - Functions can return multiple values as tuples
*argsaccepts variable number of arguments- Lambda functions are one-line anonymous functions, useful for
sorted(),filter(),map() if __name__ == "__main__"separates library code from test code- Save reusable functions in a
.pymodule (likegolf_utils.py)
Next topic: Working with Files — reading and writing golf data in CSV and JSON.